﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using System.Data.SqlServerCe;
using OverStore.Configuration.Builder;
using OverStore.Configuration.Components;
using OverStore.Sql.TableSchema;
using System.Diagnostics;
using System.Reflection;
using OverStore.Configuration.Builder.Fluent;
using OverStore.Runtime.Storage;
using System.Runtime.CompilerServices;

namespace OverStore.TutorialAsTest.UnitTest.Performance
{
    [TestClass]
    public class Performance
    {
        private static IPersistenceSessionFactory CreateConfiguration(string connectionString)
        {
            return OverStoreFluent.Configure()
                     .NoLogging() //.LogToConsole()
                     .ConnectToDatabase<SqlCeConnection>(connectionString, ProcessOpenedReadersMode.ForciblyCloseReaders)
                     .CustomClassRepository<BigTableEntity, int>()
                         .DelegateDepot()
                            .CreateInstance(CreateBigTableEntry)
                            .StoreIdentifier(ApplyBitTableEntryIdentifier)
                            .WithIdentifierGenerator(Component.IdentifierGenerator.Delegate<BigTableEntity, int>(NextIdentifier, -1))
                        .EndDepot()
                        .WithMapper(Component.Mapper.Delegate<BigTableEntity>(GetBigEntityData, ApplyBigEntityData))
                        .CommandsFromTable(false, Component.CreateTable("BigTable")
                                             .Key<int>("Id")
                                             .Column<Guid>("Identifier")
                                             .Column<string>("Name")
                                             .Column<string>("Description")
                                             .Column<DateTime>("CreatedAt")
                                             .Column<DateTime>("UpdatedAt"))
                        .WithChangesTracker(Component.InstanceChangesTracker.Delegate<BigTableEntity, int, int>(GetBigEntityChangeState))
                        .ConfigureAttacher()
                            .ExtractIdentifier(e => e.Id)
                            .AlwaysGenerateNewIdentifier()
                            .InstanceIsNewIf(e => e.Id == 0)
                            .ForceDataChanged()
                        .End()
                        .ParameterlessReader(true)
                            .ReadCommand()
                                .UseSqlCommand("SELECT * FROM BigTable")
                            .EndCommand()
                            .DelegateIdentifierProvider(r => r.GetInt32("Id"))
                        .EndReader()
                     .EndObjectRepository()
                .End();
        }

        private static IPersistenceSessionFactory CreateConfigurationTableMapping(string connectionString)
        {
            return OverStoreFluent.Configure()
                        .LogToDebug()
                        .ConnectToDatabase<SqlCeConnection>(connectionString, ProcessOpenedReadersMode.ForciblyCloseReaders)
                        .MapClassToTable<BigTableEntity, int>("BigTable")
                            .MapKey("Id", e => e.Id, (e, id) => e.Id = id, Component.IdentifierGenerator.Seq<BigTableEntity>(-1, -1))
                            .MapScalarValue<Guid>("Identifier", e => e.Identifier, (e, v) => e.Identifier = v)
                            .MapScalarValue<string>("Name", e => e.Name, (e, v) => e.Name = v)
                            .MapNullableScalarValue<string>("Description", e => e.Description, (e, v) => e.Description = v)
                            .MapScalarValue<DateTime>("CreatedAt", e => e.CreatedAt, (e, v) => e.CreatedAt = v)
                            .MapScalarValue<DateTime>("UpdatedAt", e => e.UpdatedAt, (e, v) => e.UpdatedAt = v)
                            .ConfigureAttacher()
                                .ExtractIdentifier(e => e.Id)
                                .AlwaysGenerateNewIdentifier()
                                .InstanceIsNewIf((e, p) => p.GetInstanceOrDefault<BigTableEntity, int>(e.Id) == null)
                                .ForceDataChanged()
                            .End()
                        .EndTableMapping()
                    .End();
        }

        private static int NextIdentifier(BigTableEntity item, int lastIdentifier)
        {
            return lastIdentifier - 1;
        }

        private static void ApplyBitTableEntryIdentifier(BigTableEntity item, int identifier)
        {
            item.Id = identifier;
        }

        private static BigTableEntity CreateBigTableEntry()
        {
            return new BigTableEntity();
        }

        private static int GetBigEntityChangeState(BigTableEntity item)
        {
            unchecked
            {
                return item.Id + item.Identifier.GetHashCode() +
                    //EqualityComparer<string>.Default.GetHashCode(item.Name) +
                    //EqualityComparer<string>.Default.GetHashCode(item.Description) +
                        item.Name.GetHashCode() +
                        item.Description == null ? 0 : item.Description.GetHashCode() +
                        item.CreatedAt.GetHashCode() +
                        item.UpdatedAt.GetHashCode();
            }
        }

        private static void ApplyBigEntityData(OverStore.Runtime.Storage.TypedRecord data, BigTableEntity item, OverStore.Runtime.PersistenceServiceProvider.IRegistry registry)
        {
            item.Id = data.GetInt32("Id");
            item.Identifier = data.GetGuid("Identifier");
            item.Name = data.GetString("Name");
            item.Description = data.GetString("Description");
            item.CreatedAt = data.GetDateTime("CreatedAt");
            item.UpdatedAt = data.GetDateTime("UpdatedAt");
        }

        private static object GetBigEntityData(BigTableEntity item, string name, OverStore.Runtime.PersistenceServiceProvider.IRegistry registry)
        {
            var sc = StringComparer.Ordinal;
            if ("Id" == name)
                return item.Id;
            if ("Identifier" == name)
                return item.Identifier;
            if ("Name" == name)
                return item.Name;
            if ("Description" == name)
                return item.Description;
            if ("CreatedAt" == name)
                return item.CreatedAt;
            if ("UpdatedAt" == name)
                return item.UpdatedAt;
            if ("orig__Id" == name || "orig__oid" == name)
                return registry.GetIdentifier<BigTableEntity, int>(item);
            return Missing.Value;
        }

        private void ClearBigTable(string connectionString)
        {
            using (var connection = new SqlCeConnection(connectionString))
            using (var command = connection.CreateCommand())
            {
                command.CommandText = "DELETE FROM BigTable";
                command.CommandType = System.Data.CommandType.Text;
                connection.Open();
                command.ExecuteNonQuery();
                connection.Close();
            }
        }

        private void FillBigTableWithDataUsingDbCommand(string connectionString, int count)
        {
            using (var connection = new SqlCeConnection(connectionString))
            using (var command = connection.CreateCommand())
            {
                command.CommandText = "INSERT INTO BigTable(Id, Identifier, Name, Description, CreatedAt, UpdatedAt) " +
                                      "VALUES (@Id, @Identifier, @Name, @Description, @CreatedAt, @UpdatedAt)";
                command.CommandType = System.Data.CommandType.Text;
                command.Parameters.Add(new SqlCeParameter("Id", System.Data.SqlDbType.Int));
                command.Parameters.Add(new SqlCeParameter("Identifier", System.Data.SqlDbType.UniqueIdentifier));
                command.Parameters.Add(new SqlCeParameter("Name", System.Data.SqlDbType.NVarChar));
                command.Parameters.Add(new SqlCeParameter("Description", System.Data.SqlDbType.NVarChar));
                command.Parameters.Add(new SqlCeParameter("CreatedAt", System.Data.SqlDbType.DateTime));
                command.Parameters.Add(new SqlCeParameter("UpdatedAt", System.Data.SqlDbType.DateTime));
                connection.Open();
                command.Prepare();

                for (int i = 0; i < count; i++)
                {
                    command.Parameters["Id"].Value = i;
                    command.Parameters["Identifier"].Value = Guid.NewGuid();
                    command.Parameters["Name"].Value = "Name" + i;
                    command.Parameters["Description"].Value = new String('2', 500);
                    command.Parameters["CreatedAt"].Value = DateTime.Now;
                    command.Parameters["UpdatedAt"].Value = DateTime.Now;
                    command.ExecuteNonQuery();
                }
                connection.Close();
            }
        }

        private void FillBigTableWithDataUsingOverStore(string connectionString, int count)
        {
            using (var session = CreateConfiguration(connectionString).CreateSession())
            {
                for (int i = 0; i < count; i++)
                {
                    var item = new BigTableEntity();
                    item.Id = i;
                    item.Identifier = Guid.NewGuid();
                    item.Name = "Name" + i;
                    item.Description = new String('2', 500);
                    item.CreatedAt = DateTime.Now;
                    item.UpdatedAt = DateTime.Now;
                    session.Add(item);
                }
                session.SaveAll();
            }
        }

        public class BigTableEntity
        {
            public int Id { get; set; }

            public Guid Identifier { get; set; }

            public string Name { get; set; }

            public string Description { get; set; }

            public DateTime CreatedAt { get; set; }

            public DateTime UpdatedAt { get; set; }
        }

        public IEnumerable<BigTableEntity> ReadBigTableWithDbReader(string connectionString)
        {
            using (var connection = new SqlCeConnection(connectionString))
            using (var command = connection.CreateCommand())
            {
                command.CommandText = "SELECT Id, Identifier, Name, Description, CreatedAt, UpdatedAt FROM BigTable";
                command.CommandType = System.Data.CommandType.Text;

                var list = new List<BigTableEntity>();
                connection.Open();

                using (var reader = command.ExecuteReader())
                {
                    while (reader.Read())
                    {
                        var item = new BigTableEntity();
                        item.Id = reader.GetInt32(0);
                        item.Identifier = reader.GetGuid(1);
                        item.Name = reader.GetString(2);
                        item.Description = reader.GetString(3);
                        item.CreatedAt = reader.GetDateTime(4);
                        item.UpdatedAt = reader.GetDateTime(5);
                        list.Add(item);
                    }
                    reader.Close();
                }
                connection.Close();
                return list;
            }
        }

        [MethodImpl(MethodImplOptions.NoInlining)]
        public IEnumerable<BigTableEntity> ReadBigTableWithOverStore(IPersistenceSession session)
        {
            return session.CreateReader<BigTableEntity>().ToList();
        }

        [TestMethod]
        public void __ReadPerformanceComparsion()
        {
            var connectionString = TestHelper.CreatePerformanceDbCopyAndBuildConnectionString();
            ClearBigTable(connectionString);
            FillBigTableWithDataUsingDbCommand(connectionString, 5000);

            TimeSpan overStoreElapsed;
            TimeSpan dbReaderElapsed;
            {
                using (var session = CreateConfiguration(connectionString).CreateSession())
                {
                    var sw = Stopwatch.StartNew();
                    //var list = session.CreateReader<BigTableEntity>().ToList();
                    ReadBigTableWithOverStore(session);
                    sw.Stop();
                    overStoreElapsed = sw.Elapsed;
                }
            }
            {
                var sw = Stopwatch.StartNew();
                ReadBigTableWithDbReader(connectionString);
                sw.Stop();
                dbReaderElapsed = sw.Elapsed;
            }
            var ratio = overStoreElapsed.TotalMilliseconds / dbReaderElapsed.TotalMilliseconds;
            Assert.IsTrue(ratio < 2.0, "Reading with OverStore is more then 2 times slower ({0}) then db reader", ratio);
            Console.WriteLine("Reading with OverStore slower then DbReader in {0} times.", ratio);
        }

        [TestMethod]
        public void __InsertPerformanceComparsion()
        {
            var connectionString = TestHelper.CreatePerformanceDbCopyAndBuildConnectionString();
            TimeSpan overStoreElapsed;
            TimeSpan dbCommandElapsed;
            int count = 5000;
            ClearBigTable(connectionString);
            {
                var sw = Stopwatch.StartNew();
                FillBigTableWithDataUsingOverStore(connectionString, count);
                sw.Stop();
                overStoreElapsed = sw.Elapsed;
            }
            ClearBigTable(connectionString);
            {
                var sw = Stopwatch.StartNew();
                FillBigTableWithDataUsingDbCommand(connectionString, count);
                sw.Stop();
                dbCommandElapsed = sw.Elapsed;
            }
            var ratio = overStoreElapsed.TotalMilliseconds / dbCommandElapsed.TotalMilliseconds;
            Assert.IsTrue(ratio < 2.0, "Inserting with OverStore ({0}) is more then 2 times slower ({2}) then inserting with db command ({1}).",
                overStoreElapsed.TotalMilliseconds, dbCommandElapsed.TotalMilliseconds, ratio);
            Console.WriteLine("Inserting with OverStore ({0}) {2} times slower then inserting with db command ({1}).", overStoreElapsed.TotalMilliseconds, dbCommandElapsed.TotalMilliseconds, ratio);
        }

        [TestMethod]
        public void InsertWithOverStore_ProfilingOnly()
        {
            var connectionString = TestHelper.CreatePerformanceDbCopyAndBuildConnectionString();
            ClearBigTable(connectionString);
            FillBigTableWithDataUsingOverStore(connectionString, 5000);
        }

        [TestMethod]
        public void AttachRandomData_Profiling()
        {
            var cs = TestHelper.CreatePerformanceDbCopyAndBuildConnectionString();
            using (var session = CreateConfigurationTableMapping(cs).CreateSession())
            using(var scope = session.OpenScope())
            {
                for (int i = 0; i < 1000; i++)
                {
                    var entity = new BigTableEntity
                    {
                        Identifier = Guid.NewGuid(),
                        CreatedAt = DateTime.Now,
                        Name = i.ToString(),
                        UpdatedAt = DateTime.Now
                    };
                    var attached = session.Attach(entity);
                }
                session.SaveAll();
                scope.Complete();
            }
        }
    }
}
